#pragma once
#include <assert.h>

#include <iostream>
#include <map>
#include <string>
#include <vector>

class Dynamic {
 public:
  enum class Type { Null, Bool, Double, Int64, String, Array, Object };

  Dynamic() : type_(Type::Null), data_(nullptr) {}

  Dynamic(bool value) : type_(Type::Bool), data_(new bool(value)) {}

  Dynamic(double value) : type_(Type::Double), data_(new double(value)) {}

  Dynamic(int64_t value) : type_(Type::Int64), data_(new int64_t(value)) {}
  Dynamic(const std::string& value) : type_(Type::String), data_(new std::string(value)) {}

  Dynamic(const std::vector<Dynamic>& value) : type_(Type::Array), data_(new std::vector<Dynamic>(value)) {}

  Dynamic(const std::map<std::string, Dynamic>& value)
      : type_(Type::Object), data_(new std::map<std::string, Dynamic>(value)) {}

  Dynamic(const Dynamic& other) : type_(other.getType()) { copy(other); }

  void copy(const Dynamic& other) {
    switch (type_) {
      case Type::Bool:
        data_ = new bool(other.getBool());
        break;
      case Type::Double:
        data_ = new double(other.getDouble());
        break;
      case Type::Int64:
        data_ = new int64_t(other.getInt64());
        break;
      case Type::String:
        data_ = new std::string(other.getString());
        break;
      case Type::Array:
        data_ = new std::vector<Dynamic>(other.getArray());
        break;
      case Type::Object:
        data_ = new std::map<std::string, Dynamic>(other.getObject());
        break;
      default:
        break;
    }
  }

  Dynamic& operator=(const Dynamic& other) {
    if (this != &other) {
      // delete existing data
      deleteData();
      // copy new data
      type_ = other.getType();
      copy(other);
    }
    return *this;
  }

  void deleteData() {
    switch (type_) {
      case Type::Bool:
        delete reinterpret_cast<bool*>(data_);
        break;
      case Type::Double:
        delete reinterpret_cast<double*>(data_);
        break;
      case Type::Int64:
        delete reinterpret_cast<int64_t*>(data_);
        break;
      case Type::String:
        delete reinterpret_cast<std::string*>(data_);
        break;
      case Type::Array:
        delete reinterpret_cast<std::vector<Dynamic>*>(data_);
        break;
      case Type::Object:
        delete reinterpret_cast<std::map<std::string, Dynamic>*>(data_);
        break;
      default:
        break;
    }
  }

  Type getType() const { return type_; }

  bool getBool() const {
    assert(type_ == Type::Bool);
    return *reinterpret_cast<bool*>(data_);
  }

  double getDouble() const {
    assert(type_ == Type::Double);
    return *reinterpret_cast<double*>(data_);
  }

  int64_t getInt64() const {
    assert(type_ == Type::Int64);
    return *reinterpret_cast<int64_t*>(data_);
  }

  std::string getString() const {
    assert(type_ == Type::String);
    return *reinterpret_cast<std::string*>(data_);
  }

  std::vector<Dynamic> getArray() const {
    assert(type_ == Type::Array);
    return *reinterpret_cast<std::vector<Dynamic>*>(data_);
  }

  std::map<std::string, Dynamic> getObject() const {
    assert(type_ == Type::Object);
    return *reinterpret_cast<std::map<std::string, Dynamic>*>(data_);
  }

  ~Dynamic() { deleteData(); }

  // for debug
  friend std::ostream& operator<<(std::ostream& out, const Dynamic& dyn) {
    switch (dyn.getType()) {
      case Type::Bool:
        out << "Dynamic [type: bool, val: " << dyn.getBool() << "]";
        break;
      case Type::Double:
        out << "Dynamic [type: Double, val: " << dyn.getDouble() << "]";
        break;
      case Type::Int64:
        out << "Dynamic [type: Int64, val: " << dyn.getInt64() << "]";
        break;
      case Type::String:
        out << "Dynamic [type: String, val: " << dyn.getString() << "]";
        break;
      case Type::Array: {
        out << "Dynamic [type: Array, val: Array [";
        auto array = dyn.getArray();
        for (int i = 0; i < array.size(); i++) {
          out << array[i];
          if (i != array.size() - 1) {
            out << ",";
          }
        }
        out << "] ]";
        break;
      }
      case Type::Object: {
        out << "Dynamic [type: Object, val: Object [";
        auto obj = dyn.getObject();
        for (auto i = obj.begin(); i != obj.end(); i++) {
          out << i->first << ": " << i->second << ",";
        }
        out << "] ]";
        break;
      }
      default:
        out << "Dynamic [type: Null, val: Null]";
        break;
    }
    return out;
  }

 private:
  Type type_;
  void* data_;
};